package org.apache.velocity.runtime.resource.loader;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.collections.ExtendedProperties;
import org.apache.commons.lang.StringUtils;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.runtime.resource.Resource;
import org.apache.velocity.runtime.resource.util.StringResource;
import org.apache.velocity.runtime.resource.util.StringResourceRepository;
import org.apache.velocity.runtime.resource.util.StringResourceRepositoryImpl;
import org.apache.velocity.util.ClassUtils;

/**
 * Resource loader that works with Strings. Users should manually add resources
 * to the repository that is used by the resource loader instance.
 * 
 * Below is an example configuration for this loader. Note that
 * 'repository.class' is not necessary; if not provided, the factory will fall
 * back on using {@link StringResourceRepositoryImpl} as the default.
 * 
 * <pre>
 * resource.loader = string
 * string.resource.loader.description = Velocity StringResource loader
 * string.resource.loader.class = org.apache.velocity.runtime.resource.loader.StringResourceLoader
 * string.resource.loader.repository.class = org.apache.velocity.runtime.resource.util.StringResourceRepositoryImpl
 * </pre>
 * 
 * Resources can be added to the repository like this:
 * 
 * <pre>
 * <code>
 *   StringResourceRepository repo = StringResourceLoader.getRepository();
 * 
 *   String myTemplateName = "/some/imaginary/path/hello.vm";
 *   String myTemplate = "Hi, ${username}... this is some template!";
 *   repo.putStringResource(myTemplateName, myTemplate);
 * </code>
 * </pre>
 * 
 * After this, the templates can be retrieved as usual. <br>
 * <p>
 * If there will be multiple StringResourceLoaders used in an application, you
 * should consider specifying a 'string.resource.loader.repository.name = foo'
 * property in order to keep you string resources in a non-default repository.
 * This can help to avoid conflicts between different frameworks or components
 * that are using StringResourceLoader. You can then retrieve your named
 * repository like this:
 * 
 * <pre>
 * <code>
 *   StringResourceRepository repo = StringResourceLoader.getRepository("foo");
 * </code>
 * </pre>
 * 
 * and add string resources to the repo just as in the previous example.
 * </p>
 * <p>
 * If you have concerns about memory leaks or for whatever reason do not wish to
 * have your string repository stored statically as a class member, then you
 * should set 'string.resource.loader.repository.static = false' in your
 * properties. This will tell the resource loader that the string repository
 * should be stored in the Velocity application attributes. To retrieve the
 * repository, do:
 * 
 * <pre>
 * <code>
 *   StringResourceRepository repo = velocityEngine.getApplicationAttribute("foo");
 * </code>
 * </pre>
 * 
 * If you did not specify a name for the repository, then it will be stored
 * under the class name of the repository implementation class (for which the
 * default is
 * 'org.apache.velocity.runtime.resource.util.StringResourceRepositoryImpl').
 * Incidentally, this is also true for the default statically stored repository.
 * </p>
 * <p>
 * Whether your repository is stored statically or in Velocity's application
 * attributes, you can also manually create and set it prior to Velocity
 * initialization. For a static repository, you can do something like this:
 * 
 * <pre>
 * <code>
 *   StringResourceRepository repo = new MyStringResourceRepository();
 *   repo.magicallyAddSomeStringResources();
 *   StringResourceLoader.setRepository("foo", repo);
 * </code>
 * </pre>
 * 
 * Or for a non-static repository:
 * 
 * <pre>
 * <code>
 *   StringResourceRepository repo = new MyStringResourceRepository();
 *   repo.magicallyAddSomeStringResources();
 *   velocityEngine.setApplicationAttribute("foo", repo);
 * </code>
 * </pre>
 * 
 * Then, assuming the 'string.resource.loader.repository.name' property is set
 * to 'some.name', the StringResourceLoader will use that already created
 * repository, rather than creating a new one.
 * </p>
 * 
 * @author <a href="mailto:eelco.hillenius@openedge.nl">Eelco Hillenius</a>
 * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
 * @author Nathan Bubna
 * @version $Id: StringResourceLoader.java 825302 2009-10-14 21:51:39Z nbubna $
 * @since 1.5
 */
public class StringResourceLoader extends ResourceLoader {
	/**
	 * Key to determine whether the repository should be set as the static one
	 * or not.
	 * 
	 * @since 1.6
	 */
	public static final String REPOSITORY_STATIC = "repository.static";

	/**
	 * By default, repositories are stored statically (shared across the VM).
	 * 
	 * @since 1.6
	 */
	public static final boolean REPOSITORY_STATIC_DEFAULT = true;

	/** Key to look up the repository implementation class. */
	public static final String REPOSITORY_CLASS = "repository.class";

	/** The default implementation class. */
	public static final String REPOSITORY_CLASS_DEFAULT = StringResourceRepositoryImpl.class.getName();

	/**
	 * Key to look up the name for the repository to be used.
	 * 
	 * @since 1.6
	 */
	public static final String REPOSITORY_NAME = "repository.name";

	/**
	 * The default name for string resource repositories
	 * ('org.apache.velocity.runtime.resource.util.StringResourceRepository').
	 * 
	 * @since 1.6
	 */
	public static final String REPOSITORY_NAME_DEFAULT = StringResourceRepository.class.getName();

	/** Key to look up the repository char encoding. */
	public static final String REPOSITORY_ENCODING = "repository.encoding";

	/** The default repository encoding. */
	public static final String REPOSITORY_ENCODING_DEFAULT = "UTF-8";

	protected static final Map<String, StringResourceRepository> STATIC_REPOSITORIES = new ConcurrentHashMap<>();

	// protected static final Map<String, StringResourceRepository>
	// STATIC_REPOSITORIES =
	// Collections.synchronizedMap(new HashMap<String,
	// StringResourceRepository>());

	/**
	 * Returns a reference to the default static repository.
	 */
	public static StringResourceRepository getRepository() {
		return getRepository(REPOSITORY_NAME_DEFAULT);
	}

	/**
	 * Returns a reference to the repository stored statically under the
	 * specified name.
	 * 
	 * @since 1.6
	 */
	public static StringResourceRepository getRepository(String name) {
		return STATIC_REPOSITORIES.get(name);
	}

	/**
	 * Sets the specified {@link StringResourceRepository} in static storage
	 * under the specified name.
	 * 
	 * @since 1.6
	 */
	public static void setRepository(String name, StringResourceRepository repo) {
		STATIC_REPOSITORIES.put(name, repo);
	}

	/**
	 * Removes the {@link StringResourceRepository} stored under the specified
	 * name.
	 * 
	 * @since 1.6
	 */
	public static StringResourceRepository removeRepository(String name) {
		return STATIC_REPOSITORIES.remove(name);
	}

	/**
	 * Removes all statically stored {@link StringResourceRepository}s.
	 * 
	 * @since 1.6
	 */
	public static void clearRepositories() {
		STATIC_REPOSITORIES.clear();
	}

	// the repository used internally by this resource loader
	protected StringResourceRepository repository;

	/**
	 * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#init(org.apache.commons.collections.ExtendedProperties)
	 */
	public void init(final ExtendedProperties configuration) {
		log.trace("StringResourceLoader : initialization starting.");

		// get the repository configuration info
		String repoClass = configuration.getString(REPOSITORY_CLASS, REPOSITORY_CLASS_DEFAULT);
		String repoName = configuration.getString(REPOSITORY_NAME, REPOSITORY_NAME_DEFAULT);
		boolean isStatic = configuration.getBoolean(REPOSITORY_STATIC, REPOSITORY_STATIC_DEFAULT);
		String encoding = configuration.getString(REPOSITORY_ENCODING);

		// look for an existing repository of that name and isStatic setting
		if (isStatic) {
			this.repository = getRepository(repoName);
			if (repository != null && log.isDebugEnabled()) {
				log.debug("Loaded repository '" + repoName + "' from static repo store");
			}
		} else {
			this.repository = (StringResourceRepository) rsvc.getApplicationAttribute(repoName);
			if (repository != null && log.isDebugEnabled()) {
				log.debug("Loaded repository '" + repoName + "' from application attributes");
			}
		}

		if (this.repository == null) {
			// since there's no repository under the repo name, create a new one
			this.repository = createRepository(repoClass, encoding);

			// and store it according to the isStatic setting
			if (isStatic) {
				setRepository(repoName, this.repository);
			} else {
				rsvc.setApplicationAttribute(repoName, this.repository);
			}
		} else {
			// ok, we already have a repo
			// warn them if they are trying to change the class of the
			// repository
			if (!this.repository.getClass().getName().equals(repoClass)) {
				log.debug("Cannot change class of string repository '"
						+ repoName + "' from "
						+ this.repository.getClass().getName() + " to "
						+ repoClass + ". The change will be ignored.");
			}

			// allow them to change the default encoding of the repo
			if (encoding != null && !this.repository.getEncoding().equals(encoding)) {
				if (log.isDebugEnabled()) {
					log.debug("Changing the default encoding of string repository '"
							+ repoName
							+ "' from "
							+ this.repository.getEncoding() + " to " + encoding);
				}
				this.repository.setEncoding(encoding);
			}
		}

		log.trace("StringResourceLoader : initialization complete.");
	}

	/**
	 * @since 1.6
	 */
	public StringResourceRepository createRepository(final String className, final String encoding) {
		if (log.isDebugEnabled()) {
			log.debug("Creating string repository using class " + className + "...");
		}

		StringResourceRepository repo;
		try {
			repo = (StringResourceRepository) ClassUtils.getNewInstance(className);
		} catch (ClassNotFoundException cnfe) {
			throw new VelocityException("Could not find '" + className + "'", cnfe);
		} catch (IllegalAccessException iae) {
			throw new VelocityException("Could not access '" + className + "'", iae);
		} catch (InstantiationException ie) {
			throw new VelocityException("Could not instantiate '" + className + "'", ie);
		}

		if (encoding != null) {
			repo.setEncoding(encoding);
		} else {
			repo.setEncoding(REPOSITORY_ENCODING_DEFAULT);
		}

		if (log.isDebugEnabled()) {
			log.debug("Default repository encoding is " + repo.getEncoding());
		}
		return repo;
	}

	/**
	 * Overrides superclass for better performance.
	 * 
	 * @since 1.6
	 */
	public boolean resourceExists(final String name) {
		if (name == null) {
			return false;
		}
		return (this.repository.getStringResource(name) != null);
	}

	/**
	 * Get an InputStream so that the Runtime can build a template with it.
	 * 
	 * @param name
	 *            name of template to get.
	 * @return InputStream containing the template.
	 * @throws ResourceNotFoundException
	 *             Ff template not found in the RepositoryFactory.
	 */
	public InputStream getResourceStream(final String name) throws ResourceNotFoundException {
		if (StringUtils.isEmpty(name)) {
			throw new ResourceNotFoundException("No template name provided");
		}

		StringResource resource = this.repository.getStringResource(name);

		if (resource == null) {
			throw new ResourceNotFoundException("Could not locate resource '" + name + "'");
		}

		byte[] byteArray = null;

		try {
			byteArray = resource.getBody().getBytes(resource.getEncoding());
			return new ByteArrayInputStream(byteArray);
		} catch (UnsupportedEncodingException ue) {
			throw new VelocityException("Could not convert String using encoding " + resource.getEncoding(), ue);
		}
	}

	/**
	 * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
	 */
	public boolean isSourceModified(final Resource resource) {
		StringResource original = null;
		boolean result = true;

		original = this.repository.getStringResource(resource.getName());

		if (original != null) {
			result = original.getLastModified() != resource.getLastModified();
		}

		return result;
	}

	/**
	 * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
	 */
	public long getLastModified(final Resource resource) {
		StringResource original = null;

		original = this.repository.getStringResource(resource.getName());

		return (original != null) ? original.getLastModified() : 0;
	}

}
